Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

66장. 실전 3 — 레거시 모듈 하나를 경계 밖으로 밀어내기

마지막 실전이다.

앞의 둘은 며칠짜리였다.
이번 것은 12주짜리다.

과제는 8부에서 계속 다룬 그것이다.

포인트 도메인을 떼어낼 수 있는 상태로 만든다

⚠️ 떼어내는 것이 아니다.
떼어낼 수 있게 만드는 것이다.


시작 상태

point 패키지    22 클래스, 3,100줄
흩어진 코드     order·payment·common·legacy 에 4개 파일
의존           들어옴 17곳 / 나감 3곳 (⚠️ order 역참조 — 순환)
공유 테이블     point_histories (order 도 직접 쓴다)
커버리지        19%          최근 1년 커밋  38건

39장의 로드맵에서 2차 대상이었다.
1차 알림은 6주 만에 끝났고, 거기서 배운 절차로 이번을 한다.


1주 — 조사

34·35장의 작업이다.

explorer 에게 진입점·Side Effect·point_histories 쓰기 지점·
역참조 이유를 조사시키고 docs/point-survey.md 에 저장했다.

발견한 것 중 예상 밖이 둘이었다.

⚠️ 포인트 만료 배치가 legacy 패키지에 있다
   (PointExpireJob — @Profile("batch") 로 활성화)

⚠️ 관리자 포인트 지급이 Facade를 거치지 않는다
   AdminPointService 가 Repository 직접 호출

34장에서 말한 대로,
진입점 전수 조사가 아니었으면 놓쳤다.


2~3주 — 안전망

36장의 특성화 테스트다.

가장 오래 걸렸고 가장 중요했다.

포인트가 관여하는 API 엔드포인트의 입력 조합을 나열해줘.
코드 분기를 근거로.

조합 34개가 나왔다.
36장의 우선순위대로 돈이 움직이는 경로 14개와
자주 바뀌는 경로 3개를 골랐다.

발견한 버그 3건. 고치지 않았다.

@Test
fun `현재 동작_부분취소_4회까지_허용됨_BUG의심`() { ... }

@Test
fun `현재 동작_전액포인트결제_취소시_0원_카드환불_요청됨_BUG의심`() { ... }

@Test
fun `현재 동작_만료된포인트_환급시_되살아남_BUG의심`() { ... }

🔥 세 번째는 아무도 몰랐던 버그였다.

특성화 테스트를 쓰는 과정에서 발견됐다.

⚠️ 그리고 고치지 않았다.

36장의 원칙이다.
구조 변경과 버그 수정을 동시에 하지 않는다.


4주 — 경계 카드와 계획

38장의 작업이다.

조사 문서와 데이터 소유권 표를 읽혀 경계 카드를 만들었다.
떼어내기 전에 해결할 것이 아홉 개 나왔다.

사람이 결정한 것 ① — 그중 여섯 개만 이번 범위로.

이번 범위
1. point → order 역참조 3곳 제거
2. 흩어진 4개 파일 이동
3. 계층 분리
4. order 가 point_histories 에 직접 쓰는 경로 정리
5. 경계 넘는 조인 제거
6. 아키텍처 테스트 도입

이번 범위 아님
7. 외래키 제거      → 실제 분리 계획 없으므로 유지 (43장)
8. 스키마 분리      → 동일
9. legacy 만료 배치 → 별도 티켓

43장에서 말한 판단이다.
분리 계획이 없으면 FK를 제거하지 않는다.


5~7주 — 계층 정리와 이동

40·41장의 작업이다.

41장의 분담이 실제로 갈렸다.
순수 이동은 IDE가, 판단이 필요한 것은 Agent가 했다.

의존이 적은 것부터 12단계로 나눴다.
MoneyPointBalancePointPolicy → Repository 인터페이스 → Entity 순.

각 단계가 커밋 하나였고,
git diff -M --stat 이 100%인지 매번 확인했다.

⚠️ 6단계에서 컴파일 에러가 40개 났다.

컴파일 에러가 20개를 넘었어. 멈추고 원인을 분석해줘.
고치지는 마.

41장에서 정한 중단 조건이다.

원인은 순환이었다.
PointPolicyorder 를 참조하고 있었고,
그것을 먼저 풀어야 했다.

사람이 결정한 것 ② — 6단계를 되돌리고
역참조 제거를 먼저 하기로.

git reset --hard HEAD    # 직접 실행

순서를 잘못 잡은 것이었다.

🔥 41장에서 “에러 200개는 방향이 틀렸다는 뜻” 이라고 한 것이
40개에서도 통했다.


8주 — 경계를 테스트로

42장의 작업이다.

@Test fun `point domain 은 Spring 에 의존하지 않는다`()
@Test fun `point 는 order 를 참조하지 않는다`()
@Test fun `point_histories 는 point 패키지에서만 수정한다`()

처음 돌렸을 때 위반 23건.

42장의 baseline 방식으로 예외 목록을 만들어
6건으로 줄이고 시작했다.

⚠️ 이 목록이 진척도가 됐다. 12주 끝에 2건이 남았다.


9~11주 — 데이터 경계

43장의 작업이다.

가장 조심스러운 구간이었다.

도메인 경계를 넘는 조인을 전부 찾아줘.
엔티티 연관관계도 포함해서.

7곳이 나왔고, 그중 하나가 Order 엔티티의
@ManyToOne PointBalance 였다.

43장에서 경고한 그것이다. 엔티티 연관관계도 조인이다.

조인을 제거하면서 성능을 측정했다.
주문 목록 API가 쿼리 1개·p95 180ms 에서 쿼리 3개·210ms 가 됐다.

사람이 결정한 것 ③ — 30ms 증가를 수용.

배치 조회로 N+1은 피했고, 그 이상 최적화는 하지 않기로 했다.

그리고 쓰기 주체를 정리했다.
order·admin·legacy 의 직접 INSERT를 전부
PointCommandService 경유로 바꿨다.

🔥 이 작업이 끝나자 29장의 불변식이
처음으로 실제 강제되기 시작했다.

// PointCommandService 안에서만 검증하면 된다
require(refundAmount <= usedAmount)

12주 — 준비도 정리

44장의 체크리스트를 채웠다.

경계상태
코드계층 분리 · 순환 제거 · 아키텍처 테스트 ✅ (예외 2건)
데이터조인 제거 · 쓰기 단일화 ✅ / FK·스키마 분리 ❌ (범위 아님)
트랜잭션보상 로직 정의 ✅ / 도메인별 분리 · 정합성 배치 ❌
API인터페이스 추출 · 엔티티 비노출 · sealed 결과 ✅
이벤트Consumer 멱등성 ✅ / 이벤트 스키마 ❌

절반쯤 채워졌다.


12주 동안 있었던 문제 셋

1. 기능 개발과 충돌했다

5주차에 포인트 관련 기능 요청이 들어왔다.
사람이 결정한 것 ④ — 리팩터링을 일주일 멈추고 기능을 먼저.

⚠️ 39장에서 “변경이 잦은 도메인은 충돌한다” 고 한 그것이다.
커밋 38건이라 괜찮을 줄 알았는데 겹쳤다.

2. 특성화 테스트가 깨졌다

9주차에 조인을 제거했더니 응답 필드 순서가 바뀌어
골든 마스터가 실패했다. 동작은 같고 직렬화 순서만 달랐다.

사람이 결정한 것 ⑤ — 스냅샷 갱신 승인.

36장의 규칙대로 Agent가 임의로 갱신하지 않고 보고했다.

3. 버그 3건을 언제 고칠지 논쟁했다

사람이 결정한 것 ⑥ — 리팩터링 완료 후 별도 티켓으로.
단, 만료 포인트가 되살아나는 버그는 실제 손실이 있어 우선순위를 올렸다.


12주 뒤에 얻은 것

⚠️ 포인트는 아직 떼어내지 않았다.
같은 레포, 같은 DB, 같은 프로세스다.

그런데 달라진 것이 있다.

얻은 것근거
변경 영향 범위가 좁아졌다의존 17곳 → 인터페이스 1곳
규칙이 강제된다아키텍처 테스트 3건
불변식이 실제로 지켜진다쓰기 주체 단일화
몰랐던 버그 3건 발견특성화 테스트
테스트 커버리지 19% → 63%17개 경로 고정
Agent가 더 정확해졌다좁은 Context, 강한 검증

🔥 마지막 줄이 이 책의 논지다.

경계가 명확해지자
“포인트 관련 작업” 을 시킬 때 Agent가 읽어야 할 범위가
네 패키지에서 한 패키지로 줄었다.

44장의 문장 그대로다.

나누기 위해 정리하는 것이 아니라
정리했더니 나눌 수 있게 되는 것이다.


그리고 이 책을 마치며

66장까지 왔다.

돌아보면 매 단계에서 사람이 한 일은 같았다.

방향을 정하는 것
범위를 정하는 것
되돌릴 수 없는 것을 실행하는 것
그리고 실패했을 때 하네스를 고치는 것

Agent가 한 일도 일관됐다.

전수 조사
가설 세우기
규칙대로 구현
반복 작업

이 분담이 우리 일의 새로운 모양이다.

코드를 쓰는 사람에서
코드가 잘 쓰이는 환경을 만드는 사람으로.

좋은 하네스는 좋은 Agent를 만들고,
좋은 Agent는 우리를 더 어려운 문제로 데려간다.

그 어려운 문제가
5년 된 모놀리스에 경계를 긋는 일이었다.

다음 문제는 각자의 코드베이스에 있다.


이 장의 핵심

  • 목표는 떼어내는 것이 아니라 떼어낼 수 있게 만드는 것이었다
  • 진입점 전수 조사가 아니었으면 배치와 관리자 경로를 놓쳤다
  • 특성화 테스트를 쓰는 과정에서 아무도 몰랐던 버그가 나왔다
  • 발견한 버그를 고치지 않고 테스트 이름에 표시했다
  • 컴파일 에러 40개에서 멈추고 되돌린 것이 옳았다 — 순서가 틀렸다
  • baseline 예외 목록이 23건에서 2건으로 줄어드는 것이 진척도였다
  • 엔티티 연관관계도 조인이었다
  • 조인 제거로 30ms 느려졌고 그것을 수용하기로 결정했다
  • 쓰기 주체를 모으자 불변식이 처음으로 실제 강제됐다
  • 12주 뒤에도 떼어내지 않았지만 여섯 가지를 얻었다
  • 경계가 명확해지자 Agent가 읽어야 할 범위가 4분의 1로 줄었다